package rwt.server.core;

import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
import java.util.regex.*;

import org.slf4j.*;

/**
 * The server's configuration as loaded from the configuration file (by default, this is "config.txt" in the
 * main directory). The configuration properties are loaded and stored as publicly-accessible fields.
 * Application code may read these properties, but should not modify them (changes are not saved back to the
 * configuration file).
 */
public class Config
{
	private static final Logger logger = LoggerFactory.getLogger(Config.class);
	
	public static final class SensorPair
	{
		public int head;
		public int tail;
		
		public SensorPair(int head, int tail)
		{
			this.head = head;
			this.tail = tail;
		}
	}

	public final static String CONFIG_DIR_PATH = "config";
	public final static String DEFAULT_CONFIG_FILE = CONFIG_DIR_PATH + "/Default.config.properties";
	public final static String CONFIG_SUFFIX = ".config.properties";
	
	public static String messageTranslationLibrary;
	
	/** The port at which this server will listen (e.g. 443 for normal web server [HTTPS]). */
	public static int webServerPort;
	
	/** Path (relative to project base, or absolute) to the root web directory. */
	public static String webAppPath;

	public static String railUtilsBasePath;
	public static File railUtilsBaseDir;
	
	public static String shutdownScriptPath;
	public static File shutdownScriptFile;
	
	public static String railroadExePath;
	public static File railroadExeFile;
	public static String railroadArgs;
	
	/** The TCP port number at which the CS Railroad LocoBuffer server listens. */
	public static String railroadIp;
	public static int railroadPort;
	
	public static String controllerExePath;
	public static File controllerExeFile;
	public static String controllerArgs;
	
	/** The TCP port number at which the CS Railroad Controller Program (Ada) listens. */
	public static String controllerIp;
	public static int controllerPort;
	
	public static String layoutXmlPath;
	public static File layoutXmlFile;
	
	/** The list of train IDs ("physical" IDs) of the trains that are accessible to the Web Throttle system. */
	public static List<Integer> trainIds;
	public static Map<Integer, SensorPair> trainDefaultPositionMap;
	
	/** Path (full or relative) to the SQLite database file. Created if it doesn't exist. */
	public static String databaseFile;
	
	private static Properties props;
	
	/**
	 * Loads the configuration file(s). The default config file "Default.config.properties" must exist and is
	 * loaded first. Next, the secondary config is loaded and overwrites any default settings with settings
	 * that it specifies.
	 * 
	 * @param secondaryConfigPath The full or relative file path (including file name) to the config file.
	 * @throws IOException If config file does not exist at the specified path, is not a valid properties file
	 *         (see {@link java.util.Properties#load(Reader) Properties.load}), or the data type of a property
	 *         in the file is incompatible with the data type of the corresponding property variable.
	 */
	public static void load(String secondaryConfigPath) throws IOException
	{
		props = new Properties();
		
		// Load the default (primary) settings
		File primaryConfigFile = new File(DEFAULT_CONFIG_FILE);
		if (!primaryConfigFile.isFile())
			throw new FileNotFoundException("Default config file '" + primaryConfigFile + "' not found.");
		Properties primaryProps = new Properties();
		primaryProps.load(new FileReader(primaryConfigFile));
		props.putAll(primaryProps);
		
		if (secondaryConfigPath != null)
		{
			// Load the secondary settings
			File secondaryConfigFile = new File(CONFIG_DIR_PATH, secondaryConfigPath);
			if (!secondaryConfigFile.isFile())
				throw new FileNotFoundException("Secondary config file '" + secondaryConfigFile + "' not found.");
			Properties secondaryProps = new Properties();
			secondaryProps.load(new FileReader(secondaryConfigFile));
			
			// Settings from secondary config will overwrite settings from primary
			props.putAll(secondaryProps);
		}
		
		// Load configuration properties
		
		messageTranslationLibrary = getString("MessageTranslationLibrary");
		webServerPort = getInt("WebServerPort");
		webAppPath = getString("WebAppPath");
		
		railUtilsBasePath = getString("RailUtilsBasePath");
		railUtilsBaseDir = new File(railUtilsBasePath);
		if (!railUtilsBaseDir.isDirectory())
			throw new IOException("The RailUtilsBasePath in your config is not a directory.");
		
		shutdownScriptPath = getString("ShutdownScriptPath");
		shutdownScriptFile = getFile(shutdownScriptPath);
		
		railroadExePath = getString("RailroadExePath");
		railroadExeFile = getFile(railroadExePath);
		railroadArgs = getString("RailroadArgs");
		railroadIp = getString("RailroadIp");
		railroadPort = getInt("RailroadPort");
		
		controllerExePath = getString("ControllerExePath");
		controllerExeFile = getFile(controllerExePath);
		controllerArgs = getString("ControllerArgs");
		controllerIp = getString("ControllerIp");
		controllerPort = getInt("ControllerPort");
		
		layoutXmlPath = getString("LayoutXmlPath");
		layoutXmlFile = getFile(layoutXmlPath);
		databaseFile = getString("DatabaseFile");
		
		Pattern regex = Pattern.compile("\\s*(?<id>\\d+)\\s*(?:\\((?<head>\\d+)\\s+(?<tail>\\d+)\\)\\s*)?");
		
		String trainIdsProp = getString("TrainIds");
		if (trainIdsProp != null)
		{
			String[] trainsIdsArray = trainIdsProp.split("\\s*\\,\\s*");
			trainIds = new ArrayList<>(trainsIdsArray.length);
			trainDefaultPositionMap = new HashMap<>(trainsIdsArray.length * 2);
			
			for (String trainIdStr : trainsIdsArray)
			{
				Matcher m = regex.matcher(trainIdStr);
				if (m.matches())
				{
					int id = Integer.parseInt(m.group("id"));
					trainIds.add(id);
					
					String headStr = m.group("head");
					String tailStr = m.group("tail");
					
					if (headStr != null && tailStr != null)
					{
						int head = Integer.parseInt(headStr);
						int tail = Integer.parseInt(tailStr);
						trainDefaultPositionMap.put(id, new SensorPair(head, tail));
					}
				}
				else
				{
					throw new IOException("The TrainIds property in your config file could not be parsed: '"
							+ trainIdsProp + "'");
				}
			}
		}
		logger.info("Configuration loaded");
	}
	
	public static void save(String configPath)
	{
		Field[] fields = Config.class.getDeclaredFields();
		final Class<String> stringClass = String.class;
		final Class<?> intClass = int.class;
		
		Properties newProps = new Properties();
		
		for (Field field : fields)
		{
			String fieldName = field.getName();
			String propName = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
			
			try
			{
				if (field.getType().equals(stringClass) || field.getType().equals(intClass))
				{
					Object obj = field.get(null);
					String value = (obj == null) ? "" : obj.toString();
					newProps.put(propName, value);
				}
			}
			catch (Exception e)
			{
				logger.warn("Error in field name: " + fieldName, e);
			}
		}
		
		String trainIdsStr = "";
		if (Config.trainIds != null && !Config.trainIds.isEmpty())
		{
			StringBuilder sb = new StringBuilder();
			for (int id : Config.trainIds)
			{
				sb.append(id);
				SensorPair pair = Config.trainDefaultPositionMap.get(id);
				if (pair != null)
				{
					sb.append(" (");
					sb.append(pair.head);
					sb.append(" ");
					sb.append(pair.tail);
					sb.append("), ");
				}
			}
			
			sb.setLength(sb.length() - 2); // Remove trailing ", "
			trainIdsStr = sb.toString();
		}
		newProps.put("TrainIds", trainIdsStr);
		
		try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(configPath)))
		{
			newProps.store(bos, "This is an automatically generated config file.");
		}
		catch (IOException e)
		{
			logger.warn("Failed to store config file.", e);
		}
	}
	
	private static File getFile(String path) throws IOException
	{
		File f = new File(path.trim());
		if (!f.isAbsolute())
			f = new File(railUtilsBasePath, path); // Assume to be relative to railUtilsBasePath
			
		if (!f.isFile())
			throw new FileNotFoundException("File '" + f + "' not found.");
		return f;
	}
	
	private static int getInt(String key)
	{
		return Integer.parseInt(props.getProperty(key));
	}
	
	private static String getString(String key)
	{
		if (key == null)
			throw new IllegalArgumentException("Missing value for property '" + key + "'");
		return props.getProperty(key);
	}
}
